home *** CD-ROM | disk | FTP | other *** search
/ Chip 2007 January, February, March & April / Chip-Cover-CD-2007-02.iso / Pakiet bezpieczenstwa / mini Pentoo LiveCD 2006.1 / mpentoo-2006.1.iso / livecd.squashfs / usr / share / cvs / contrib / cvs_acls < prev    next >
Text File  |  2005-10-16  |  38KB  |  952 lines

  1. #! /usr/bin/perl -T
  2. # -*-Perl-*-
  3.  
  4. ###############################################################################
  5. ###############################################################################
  6. ###############################################################################
  7. #
  8. # THIS SCRIPT IS PROBABLY BROKEN.  REMOVING THE -T SWITCH ON THE #! LINE ABOVE
  9. # WOULD FIX IT, BUT THIS IS INSECURE.  WE RECOMMEND FIXING THE ERRORS WHICH THE
  10. # -T SWITCH WILL CAUSE PERL TO REPORT BEFORE RUNNING THIS SCRIPT FROM A CVS
  11. # SERVER TRIGGER.  PLEASE SEND PATCHES CONTAINING THE CHANGES YOU FIND
  12. # NECESSARY TO RUN THIS SCRIPT WITH THE TAINT-CHECKING ENABLED BACK TO THE
  13. # <bug-cvs@gnu.org> MAILING LIST.
  14. #
  15. # For more on general Perl security and taint-checking, please try running the
  16. # `perldoc perlsec' command.
  17. #
  18. ###############################################################################
  19. ###############################################################################
  20. ###############################################################################
  21.  
  22. =head1 Name
  23.  
  24. cvs_acls - Access Control List for CVS
  25.  
  26. =head1 Synopsis
  27.  
  28. In 'commitinfo':
  29.  
  30.   repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f <logfile>]
  31.  
  32. where:
  33.  
  34.   -d  turns on debug information
  35.   -u  passes the client-side userId to the cvs_acls script
  36.   -f  specifies an alternate filename for the restrict_log file
  37.  
  38. In 'cvsacl':
  39.  
  40.   {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
  41.  
  42. where:
  43.  
  44.   allow|deny - allow: commits are allowed; deny: prohibited
  45.   user          - userId to be allowed or restricted
  46.   repos         - file or directory to be allowed or restricted
  47.   branch        - branch to be allowed or restricted
  48.  
  49. See below for examples.
  50.  
  51. =head1 Licensing
  52.  
  53. cvs_acls - provides access control list functionality for CVS
  54.   
  55. Copyright (c) 2004 by Peter Connolly <peter.connolly@cnet.com>  
  56. All rights reserved.
  57.  
  58. This program is free software; you can redistribute it and/or modify  
  59. it under the terms of the GNU General Public License as published by  
  60. the Free Software Foundation; either version 2 of the License, or  
  61. (at your option) any later version. 
  62.  
  63. This program is distributed in the hope that it will be useful,  
  64. but WITHOUT ANY WARRANTY; without even the implied warranty of  
  65. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
  66. GNU General Public License for more details.  
  67.  
  68. You should have received a copy of the GNU General Public License  
  69. along with this program; if not, write to the Free Software  
  70. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  71.  
  72. =head1 Description
  73.  
  74. This script--cvs_acls--is invoked once for each directory within a 
  75. "cvs commit". The set of files being committed for that directory as 
  76. well as the directory itself, are passed to this script.  This script 
  77. checks its 'cvsacl' file to see if any of the files being committed 
  78. are on the 'cvsacl' file's restricted list.  If any of the files are
  79. restricted, then the cvs_acls script passes back an exit code of 1
  80. which disallows the commits for that directory.  
  81.  
  82. Messages are returned to the committer indicating the file(s) that 
  83. he/she are not allowed to committ.  Additionally, a site-specific 
  84. set of messages (e.g., contact information) can be included in these 
  85. messages.
  86.  
  87. When a commit is prohibited, log messages are written to a restrict_log
  88. file in $CVSROOT/CVSROOT.  This default file can be redirected to 
  89. another destination.
  90.  
  91. The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.
  92.  
  93. =head1 Enhancements
  94.  
  95. This section lists the bug fixes and enhancements added to cvs_acls
  96. that make up the current cvs_acls.
  97.  
  98. =head2 Fixed Bugs
  99.  
  100. This version attempts to get rid the following bugs from the
  101. original version of cvs_acls:
  102.  
  103. =over 2
  104.  
  105. =item *
  106. Multiple entries on an 'cvsacl' line will be matched individually, 
  107. instead of requiring that all commit files *exactly* match all 
  108. 'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
  109. allow *all* files (including a restricted file) to be committed.
  110.  
  111. [IMO, this basically made the original script unuseable for our 
  112. situation since any arbitrary combination of committed files could 
  113. avoid matching the 'cvsacl's entries.]
  114.  
  115. =item *
  116. Handle specific filename restrictions. cvs_acls didn't restrict
  117. individual files specified in 'cvsacl'.
  118.  
  119. =item *
  120. Correctly handle multiple, specific filename restrictions
  121.  
  122. =item *
  123. Prohibit mix of dirs and files on a single 'cvsacl' line
  124. [To simplify the logic and because this would be normal usage.]
  125.  
  126. =item *
  127. Correctly handle a mixture of branch restrictions within one work
  128. directory
  129.  
  130. =item *
  131. $CVSROOT existence is checked too late
  132.  
  133. =item *
  134. Correctly handle the CVSROOT=:local:/... option (useful for 
  135. interactive testing)
  136.  
  137. =item *
  138. Replacing shoddy "$universal_off" logic 
  139. (Thanks to Karl-Konig Konigsson for pointing this out.)
  140.  
  141. =back
  142.  
  143. =head2 Enhancements
  144.  
  145. =over 2
  146.  
  147. =item *
  148. Checks modules in the 'cvsacl' file for valid files and directories
  149.  
  150. =item *
  151. Accurately report restricted entries and their matching patterns
  152.  
  153. =item *
  154. Simplified and commented overly complex PERL REGEXPs for readability 
  155. and maintainability
  156.  
  157. =item *
  158. Skip the rest of processing if a mismatch on portion of the 'cvsacl' line
  159.  
  160. =item *
  161. Get rid of opaque "karma" messages in favor of user-friendly messages
  162. that describe which user, file(s) and branch(es) were disallowed.
  163.  
  164. =item *
  165. Add optional 'restrict_msg' file for additional, site-specific 
  166. restriction messages.
  167.  
  168. =item *
  169. Take a "-u" parameter for $USER from commit_prep so that the script
  170. can do restrictions based on the client-side userId rather than the
  171. server-side userId (usually 'cvs').
  172.  
  173. (See discussion below on "Admin Setup" for more on this point.)
  174.  
  175. =item *
  176. Added a lot more debug trace 
  177.  
  178. =item *
  179. Tested these restrictions with concurrent use of pserver and SSH
  180. access to model our transition from pserver to ext access.
  181.  
  182. =item *
  183. Added logging of restricted commit attempts.
  184. Restricted commits can be sent to a default file:
  185. $CVSROOT/CVSROOT/restrictlog or to one passed to the script
  186. via the -f command parameter.
  187.  
  188. =back
  189.  
  190. =head2 ToDoS 
  191.  
  192. =over 2
  193.  
  194. =item *
  195. Need to deal with pserver/SSH transition with conflicting umasks?
  196.  
  197. =item *
  198. Use a CPAN module to handle command parameters.
  199.  
  200. =item *
  201. Use a CPAN module to clone data structures.
  202.  
  203. =back
  204.  
  205. =head1 Version Information
  206.  
  207. This is not offered as a fix to the original 'cvs_acls' script since it 
  208. differs substantially in goals and methods from the original and there 
  209. are probably a significant number of people out there that still require 
  210. the original version's functionality.
  211.  
  212. The 'cvsacl' file flags of 'allow' and 'deny' were intentionally 
  213. changed to 'allow' and 'deny' because there are enough differences 
  214. between the original script's behavior and this one's that we wanted to
  215. make sure that users will rethink their 'cvsacl' file formats before
  216. plugging in this newer script.
  217.  
  218. Please note that there has been very limited cross-platform testing of 
  219. this script!!! (We did not have the time or resources to do exhaustive
  220. cross-platform testing.)
  221.  
  222. It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
  223. Additionally, it was built and tested under Red Hat Linux 7.3 using 
  224. PERL 5.6.1.
  225.  
  226. $Id: cvs_acls.in,v 1.7 2005/04/14 15:45:00 dprice Exp $
  227.  
  228. This version is based on the 1.11.13 version of cvs_acls
  229. peter.connolly@cnet.com (Peter Connolly) 
  230.  
  231.   Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
  232.   Branch specific controls added by voisine@bytemobile.com (Aaron Voisine)
  233.  
  234. =head1 Installation
  235.  
  236. To use this program, do the following four things:
  237.  
  238. 0. Install PERL, version 5.6.1 or 5.8.0.
  239.  
  240. 1. Admin Setup:
  241.  
  242.    There are two choices here. 
  243.  
  244.    a) The first option is to use the $ENV{"USER"}, server-side userId
  245.       (from the third column of your pserver 'passwd' file) as the basis for 
  246.       your restrictions.  In this case, you will (at a minimum) want to set
  247.       up a new "cvsadmin" userId and group on the pserver machine.  
  248.       CVS administrators will then set up their 'passwd' file entries to
  249.       run either as "cvs" (for regular users) or as "cvsadmin" (for power 
  250.       users).  Correspondingly, your 'cvsacl' file will only list 'cvs'
  251.       and 'cvsadmin' as the userIds in the second column.
  252.  
  253.       Commentary: A potential weakness of this is that the xinetd 
  254.       cvspserver process will need to run as 'root' in order to switch 
  255.       between the 'cvs' and the 'cvsadmin' userIds.  Some sysadmins don't
  256.       like situations like this and may want to chroot the process.
  257.       Talk to them about this point...
  258.  
  259.    b) The second option is to use the client-side userId as the basis for
  260.       your restrictions.  In this case, all the xinetd cvspserver processes 
  261.       can run as userId 'cvs' and no 'root' userId is required.  If you have
  262.       a 'passwd' file that lists 'cvs' as the effective run-time userId for
  263.       all your users, then no changes to this file are needed.  Your 'cvsacl'
  264.       file will use the individual, client-side userIds in its 2nd column.
  265.  
  266.       As long as the userIds in pserver's 'passwd' file match those userIds 
  267.       that your Linux server know about, this approach is ideal if you are 
  268.       planning to move from pserver to SSH access at some later point in time.
  269.       Just by switching the CVSROOT var from CVSROOT=:pserver:<userId>... to 
  270.       CVSROOT=:ext:<userId>..., users can switch over to SSH access without
  271.       any other administrative changes.  When all users have switched over to
  272.       SSH, the inherently insecure xinetd cvspserver process can be disabled.
  273.       [https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_2.html#SEC32]
  274.  
  275.       :TODO: The only potential glitch with the SSH approach is the possibility 
  276.       that each user can have differing umasks that might interfere with one 
  277.       another, especially during a transition from pserver to SSH.  As noted
  278.       in the ToDo section, this needs a good strategy and set of tests for that 
  279.       yet...
  280.  
  281. 2. Put two lines, as the *only* non-comment lines, in your commitinfo file:
  282.  
  283.    ALL $CVSROOT/CVSROOT/commit_prep 
  284.    ALL $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER ][-f <logfilename>]
  285.  
  286.    where "-d" turns on debug trace
  287.          "-u $USER" passes the client-side userId to cvs_acls 
  288.          "-f <logfilename"> overrides the default filename used to log
  289.                             restricted commit attempts.
  290.  
  291.    (These are handled in the processArgs() subroutine.)
  292.  
  293. If you are using client-side userIds to restrict access to your 
  294. repository, make sure that they are in this order since the commit_prep 
  295. script is required in order to pass the $USER parameter.
  296.  
  297. A final note about the repository matching pattern.  The example above
  298. uses "ALL" but note that this means that the cvs_acls script will run
  299. for each and every commit in your repository.  Obviously, in a large
  300. repository this adds up to a lot of overhead that may not be necesary. 
  301. A better strategy is to use a repository pattern that is more specific 
  302. to the areas that you wish to secure.
  303.  
  304. 3. Install this file as $CVSROOT/CVSROOT/cvs_acls and make it executable.
  305.  
  306. 4. Create a file named CVSROOT/cvsacl and optionally add it to
  307.    CVSROOT/checkoutlist and check it in.  See the CVS manual's
  308.    administrative files section about checkoutlist.  Typically:
  309.  
  310.    $ cvs checkout CVSROOT
  311.    $ cd CVSROOT
  312.    [ create the cvsacl file, include 'commitinfo' line ]
  313.    [ add cvsacl to checkoutlist ]
  314.    $ cvs add cvsacl
  315.    $ cvs commit -m 'Added cvsacl for use with cvs_acls.' cvsacl checkoutlist
  316.  
  317. Note: The format of the 'cvsacl' file is described in detail immediately 
  318. below but here is an important set up point:
  319.  
  320.    Make sure to include a line like the following:
  321.  
  322.      deny||CVSROOT/commitinfo CVSROOT/cvsacl
  323.      allow|cvsadmin|CVSROOT/commitinfo CVSROOT/cvsacl
  324.  
  325.    that restricts access to commitinfo and cvsacl since this would be one of
  326.    the easiest "end runs" around this ACL approach. ('commitinfo' has the 
  327.    line that executes the cvs_acls script and, of course, all the 
  328.    restrictions are in 'cvsacl'.)
  329.  
  330. 5. (Optional) Create a 'restrict_msg' file in the $CVSROOT/CVSROOT directory.
  331.    Whenever there is a restricted file or dir message, cvs_acls will look 
  332.    for this file and, if it exists, print its contents as part of the 
  333.    commit-denial message.  This gives you a chance to print any site-specific
  334.    information (e.g., who to call, what procedures to look up,...) whenever
  335.    a commit is denied.
  336.  
  337. =head1 Format of the cvsacl file
  338.  
  339. The 'cvsacl' file determines whether you may commit files.  It contains lines
  340. read from top to bottom, keeping track of whether a given user, repository
  341. and branch combination is "allowed" or "denied."  The script will assume 
  342. "allowed" on all repository paths until 'allow' and 'deny' rules change 
  343. that default.  
  344.  
  345. The normal pattern is to specify an 'deny' rule to turn off
  346. access to ALL users, then follow it with a matching 'allow' rule that will 
  347. turn on access for a select set of users.  In the case of multiple rules for
  348. the same user, repository and branch, the last one takes precedence.
  349.  
  350. Blank lines and lines with only comments are ignored.  Any other lines not 
  351. beginning with "allow" or "deny" are logged to the restrict_log file.
  352.  
  353. Lines beginning with "allow" or "deny" are assumed to be '|'-separated
  354. triples: (All spaces and tabs are ignored in a line.)
  355.  
  356.   {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
  357.  
  358.    1. String starting with "allow" or "deny".
  359.    2. Optional, comma-separated list of usernames.
  360.    3. Optional, comma-separated list of repository pathnames.
  361.       These are pathnames relative to $CVSROOT.  They can be directories or
  362.       filenames.  A directory name allows or restricts access to all files and
  363.       directories below it. One line can have either directories or filenames
  364.       but not both.
  365.    4. Optional, comma-separated list of branch tags.
  366.       If not specified, all branches are assumed. Use HEAD to reference the
  367.       main branch.
  368.  
  369. Example:  (Note: No in-line comments.)
  370.  
  371.    # ----- Make whole repository unavailable.
  372.    deny
  373.  
  374.    # ----- Except for user "dgg".
  375.    allow|dgg
  376.  
  377.    # ----- Except when "fred" or "john" commit to the 
  378.    #       module whose repository is "bin/ls"
  379.    allow|fred, john|bin/ls
  380.  
  381.    # ----- Except when "ed" commits to the "stable" 
  382.    #       branch of the "bin/ls" repository
  383.    allow|ed|/bin/ls|stable
  384.  
  385. =head1 Program Logic
  386.  
  387. CVS passes to @ARGV an absolute directory pathname (the repository
  388. appended to your $CVSROOT variable), followed by a list of filenames
  389. within that directory that are to be committed.
  390.  
  391. The script walks through the 'cvsacl' file looking for matches on 
  392. the username, repository and branch.
  393.  
  394. A username match is simply the user's name appearing in the second
  395. column of the cvsacl line in a space-or-comma separate list. If
  396. blank, then any user will match.
  397.  
  398. A repository match:
  399.  
  400. =over 2
  401.  
  402. =item *
  403. Each entry in the modules section of the current 'cvsacl' line is 
  404. examined to see if it is a dir or a file. The line must have 
  405. either files or dirs, but not both. (To simplify the logic.)
  406.  
  407. =item *
  408. If neither, then assume the 'cvsacl' file was set up in error and
  409. skip that 'allow' line.
  410.  
  411. =item *
  412. If a dir, then each dir pattern is matched separately against the 
  413. beginning of each of the committed files in @ARGV. 
  414.  
  415. =item *
  416. If a file, then each file pattern is matched exactly against each
  417. of the files to be committed in @ARGV.
  418.  
  419. =item *
  420. Repository and branch must BOTH match together. This is to cover
  421. the use case where a user has multiple branches checked out in
  422. a single work directory. Commit files can be from different
  423. branches.
  424.  
  425. A branch match is either:
  426.  
  427. =over 4
  428.  
  429. =item *
  430. When no branches are listed in the fourth column. ("Match any.")
  431.  
  432. =item *
  433. All elements from the fourth column are matched against each of 
  434. the tag names for $ARGV[1..$#ARGV] found in the %branches file.
  435.  
  436. =back
  437.  
  438. =item *
  439. 'allow' match remove that match from the tally map.
  440.  
  441. =item *
  442. Restricted ('deny') matches are saved in the %repository_matches 
  443. table.
  444.  
  445. =item *
  446. If there is a match on user, repository and branch:
  447.  
  448.   If repository, branch and user match
  449.     if 'deny'
  450.       add %repository_matches entries to %restricted_entries
  451.     else if 'allow'
  452.       remove %repository_matches entries from %restricted_entries
  453.  
  454. =item *
  455. At the end of all the 'cvsacl' line checks, check to see if there
  456. are any entries in the %restricted_entries.  If so, then deny the
  457. commit.
  458.  
  459. =back
  460.  
  461. =head2 Pseudocode
  462.  
  463.      read CVS/Entries file and create branch{file}->{branch} hash table
  464.    + for each 'allow' and 'deny' line in the 'cvsacl' file:
  465.    |   user match?   
  466.    |     - Yes: set $user_match       = 1;
  467.    |   repository and branch match?
  468.    |     - Yes: add to %repository_matches;
  469.    |   did user, repository match?
  470.    |     - Yes: if 'deny' then 
  471.    |                add %repository_matches -> %restricted_entries
  472.    |            if 'allow'   then 
  473.    |                remove %repository_matches <- %restricted_entries
  474.    + end for loop
  475.      any saved restrictions?
  476.        no:  exit, 
  477.             set exit code allowing commits and exit
  478.        yes: report restrictions, 
  479.             set exit code prohibiting commits and exit
  480.  
  481. =head2 Sanity Check
  482.  
  483.   1) file allow trumps a dir deny
  484.      deny||java/lib
  485.      allow||java/lib/README
  486.   2) dir allow can undo a file deny
  487.      deny||java/lib/README
  488.      allow||java/lib
  489.   3) file deny trumps a dir allow
  490.      allow||java/lib
  491.      deny||java/lib/README
  492.   4) dir deny trumps a file allow
  493.      allow||java/lib/README
  494.      deny||java/lib
  495.   ... so last match always takes precedence
  496.  
  497. =cut
  498.  
  499. $debug = 0;                     # Set to 1 for debug messages
  500.  
  501. %repository_matches = ();       # hash of match file and pattern from 'cvsacl'
  502.                                 # repository_matches --> [branch, matching-pattern]
  503.                                 # (Used during module/branch matching loop)
  504.  
  505. %restricted_entries = ();       # hash table of restricted commit files (from @ARGV)
  506.                                 # restricted_entries --> branch
  507.                                 # (If user/module/branch all match on an 'deny'
  508.                                 #  line, then entries added to this map.)
  509.  
  510. %branch;                        # hash table of key: commit file; value: branch
  511.                                 # Built from ".../CVS/Entries" file of directory 
  512.                                 # currently being examined
  513.  
  514. # ---------------------------------------------------------------- get CVSROOT
  515. $cvsroot = $ENV{'CVSROOT'};
  516. die "Must set CVSROOT\n" if !$cvsroot;
  517. if ($cvsroot =~ /:([\/\w]*)$/) { # Filter ":pserver:", ":local:"-type prefixes
  518.     $cvsroot = $1; 
  519. }
  520.  
  521. # ------------------------------------------------------------- set file paths
  522. $entries = "CVS/Entries";                                # client-side file???
  523. $cvsaclfile = $cvsroot . "/CVSROOT/cvsacl";
  524. $restrictfile = $cvsroot . "/CVSROOT/restrict_msg";
  525. $restrictlog = $cvsroot . "/CVSROOT/restrict_log";
  526.  
  527. # --------------------------------------------------------------- process args
  528. $user_name = processArgs(\@ARGV);
  529.  
  530. print("$$ \@ARGV after processArgs is: @ARGV.\n") if $debug;
  531. print("$$ ========== Begin $PROGRAM_NAME for \"$ARGV[0]\" repository. ========== \n") if $debug;
  532.  
  533. # --------------------------------------------------------------- filter @ARGV
  534. eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';"
  535.     while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV));
  536. exit 255 if $die;                 # process any variable=value switches
  537.  
  538. print("$$ \@ARGV after shift processing contains:",join("\, ",@ARGV),".\n") if $debug;
  539.  
  540. # ---------------------------------------------------------------- get cvsroot
  541. ($repository = shift) =~ s:^$cvsroot/::;
  542. grep($_ = $repository . '/' . $_, @ARGV);
  543.  
  544. print("$$ \$cvsroot is: $cvsroot.\n") if $debug;
  545. print "$$ Repos: $repository\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if $debug;
  546.  
  547. $exit_val = 0;                     # presume good exit value for commit
  548.  
  549. # ----------------------------------------------------------------------------
  550. # ---------------------------------- create hash table $branch{file -> branch}
  551. # ----------------------------------------------------------------------------
  552.  
  553. # Here's a typical Entries file:
  554. #
  555. #   /checkoutlist/1.4/Wed Feb  4 23:51:23 2004//
  556. #   /cvsacl/1.3/Tue Feb 24 23:05:43 2004//
  557. #   ...
  558. #   /verifymsg/1.1/Fri Mar 16 19:56:24 2001//
  559. #   D/backup////
  560. #   D/temp////
  561.  
  562. open(ENTRIES, $entries) || die("Cannot open $entries.\n");
  563. print("$$ File / Branch\n") if $debug;
  564. my $i = 0;
  565. while(<ENTRIES>) {
  566.     chop;
  567.     next if /^\s*$/;                    # Skip blank lines
  568.     $i = $i + 1;
  569.     if (m|
  570.           /                             # 1st slash
  571.           ([\w.-]*)                     # file name -> $1
  572.           /                             # 2nd slash
  573.           .*                            # revision number
  574.           /                             # 3rd slash
  575.           .*                            # date and time
  576.           /                             # 4th slash
  577.           .*                            # keyword
  578.           /                             # 5th slash
  579.           T?                            # 'T' constant
  580.           (\w*)                         # branch    -> #2
  581.           |x) {
  582.     $branch{$repository . '/' . $1} = ($2) ? $2 : "HEAD"; 
  583.     print "$$ CVS Entry $i: $1/$2\n" if $debug;
  584.     }
  585. }
  586. close(ENTRIES);
  587.  
  588. # ----------------------------------------------------------------------------
  589. # ------------------------------------- evaluate each active line from 'cvsacl'
  590. # ----------------------------------------------------------------------------
  591. open (CVSACL, $cvsaclfile) || exit(0);    # It is ok for cvsacl file not to exist
  592. while (<CVSACL>) {
  593.     chop;
  594.     next if /^\s*\#/;                               # skip comments
  595.     next if /^\s*$/;                                # skip blank lines
  596.     # --------------------------------------------- parse current 'cvsacl' line
  597.     print("$$ ==========\n$$ Processing \'cvsacl\' line: $_.\n") if $debug;
  598.     ($cvsacl_flag, $cvsacl_userIds, $cvsacl_modules, $cvsacl_branches) = split(/[\s,]*\|[\s,]*/, $_);
  599.  
  600.     # ------------------------------ Validate 'allow' or 'deny' line prefix
  601.     if ($cvsacl_flag !~ /^allow/ && $cvsacl_flag !~ /^deny/) {
  602.     print ("Bad cvsacl line: $_\n") if $debug;
  603.     $log_text = sprintf "Bad cvsacl line: %s", $_; 
  604.     write_restrictlog_record($log_text);
  605.         next;
  606.     }
  607.  
  608.     # -------------------------------------------------- init loop match flags
  609.     $user_match = 0;
  610.     %repository_matches = ();
  611.  
  612.     # ------------------------------------------------------------------------
  613.     # ---------------------------------------------------------- user matching
  614.     # ------------------------------------------------------------------------
  615.     # $user_name considered "in user list" if actually in list or is NULL
  616.     $user_match = (!$cvsacl_userIds || grep ($_ eq $user_name, split(/[\s,]+/,$cvsacl_userIds)));
  617.     print "$$ \$user_name: $user_name \$user_match match flag is: $user_match.\n" if $debug;
  618.     if (!$user_match) {
  619.     next;                            # no match, skip to next 'cvsacl' line
  620.     }
  621.  
  622.     # ------------------------------------------------------------------------
  623.     # ---------------------------------------------------- repository matching
  624.     # ------------------------------------------------------------------------
  625.     if (!$cvsacl_modules) {                  # blank module list = all modules
  626.     if (!$cvsacl_branches) {            # blank branch list = all branches
  627.         print("$$ Adding all modules to \%repository_matches; null " . 
  628.                   "\$cvsacl_modules and \$cvsacl_branches.\n") if $debug;
  629.         for $commit_object (@ARGV) {
  630.         $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_modules];
  631.                 print("$$ \$repository_matches{$commit_object} = " .
  632.                       "[$branch{$commit_object}, $cvsacl_modules].\n") if $debug;
  633.         }
  634.     }
  635.     else {                            # need to check for repository match
  636.         @branch_list = split (/[\s,]+/,$cvsacl_branches);
  637.         print("$$ Branches from \'cvsacl\' record: ", join(", ",@branch_list),".\n") if $debug;
  638.         for $commit_object (@ARGV) {
  639.         if (grep($branch{$commit_object}, @branch_list)) {
  640.             $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_modules];
  641.             print("$$ \$repository_matches{$commit_object} = " .
  642.                           "[$branch{$commit_object}, $cvsacl_modules].\n") if $debug;
  643.         }
  644.         }
  645.     }
  646.     }
  647.     else {
  648.     # ----------------------------------- check every argument combination
  649.     # parse 'cvsacl' modules to array
  650.     my @module_list = split(/[\s,]+/,$cvsacl_modules);
  651.         # ------------- Check all modules in list for either file or directory
  652.     my $fileType = "";
  653.     if (($fileType = checkFileness(@module_list)) eq "") {
  654.         next;                                        # skip bad file types
  655.     }
  656.     # ---------- Check each combination of 'cvsacl' modules vs. @ARGV files
  657.     print("$$ Checking matches for \@module_list: ", join("\, ",@module_list), ".\n") if $debug;
  658.     # loop thru all command-line commit objects
  659.         for $commit_object (@ARGV) {              
  660.         # loop thru all modules on 'cvsacl' line
  661.             for $cvsacl_module (@module_list) { 
  662.             print("$$ Is \'cvsacl\': $cvsacl_modules pattern in: \@ARGV " . 
  663.                       "\$commit_object: $commit_object?\n") if $debug;
  664.         # Do match of beginning of $commit_object
  665.             checkModuleMatch($fileType, $commit_object, $cvsacl_module);
  666.         } # end for commit objects
  667.     } # end for cvsacl modules
  668.     } # end if
  669.  
  670.     print("$$ Matches for: \%repository_matches: ", join("\, ", (keys %repository_matches)), ".\n") if $debug;
  671.  
  672.     # ------------------------------------------------------------------------
  673.     # ----------------------------------------------------- setting exit value
  674.     # ------------------------------------------------------------------------
  675.     if ($user_match && %repository_matches) {
  676.         print("$$ An \"$cvsacl_flag\" match on User(s): $cvsacl_userIds; Module(s):" .
  677.               " $cvsacl_modules; Branch(es): $cvsacl_branches.\n") if $debug;
  678.     if ($cvsacl_flag eq "deny") {
  679.         # Add all matches to the hash of restricted modules
  680.         foreach $commitFile (keys %repository_matches) {
  681.         print("$$ Adding \%repository_matches entry: $commitFile.\n") if $debug;
  682.         $restricted_entries{$commitFile} = $repository_matches{$commitFile}[0];
  683.         }
  684.     }
  685.     else {
  686.         # Remove all matches from the restricted modules hash
  687.         foreach $commitFile (keys %repository_matches) {
  688.         print("$$ Removing \%repository_matches entry: $commitFile.\n") if $debug;
  689.         delete $restricted_entries{$commitFile};
  690.         }
  691.     }
  692.     }
  693.     print "$$ ==== End of processing for \'cvsacl\' line: $_.\n" if $debug;
  694. }
  695. close(CVSACL);
  696.  
  697. # ----------------------------------------------------------------------------
  698. # --------------------------------------- determine final 'commit' disposition
  699. # ---------------------------------------------------------------------------- 
  700. if (%restricted_entries) {                           # any restricted entries?
  701.     $exit_val = 1;                                              # don't commit
  702.     print("**** Access denied: Insufficient authority for user: '$user_name\' " .
  703.           "to commit to \'$repository\'.\n**** Contact CVS Administrators if " .
  704.           "you require update access to these directories or files.\n");
  705.     print("**** file(s)/dir(s) restricted were:\n\t", join("\n\t",keys %restricted_entries), "\n");
  706.     printOptionalRestrictionMessage();
  707.     write_restrictlog();
  708. }
  709. elsif (!$exit_val && $debug) {
  710.     print "**** Access allowed: Sufficient authority for commit.\n";
  711. }
  712.  
  713. print "$$ ==== \$exit_val = $exit_val\n" if $debug;
  714. exit($exit_val);
  715.  
  716. # ----------------------------------------------------------------------------
  717. # -------------------------------------------------------------- end of "main"
  718. # ----------------------------------------------------------------------------
  719.  
  720.  
  721. # ----------------------------------------------------------------------------
  722. # -------------------------------------------------------- process script args
  723. # ----------------------------------------------------------------------------
  724. sub processArgs {
  725.  
  726. # This subroutine is passed a reference to @ARGV. 
  727.  
  728. # If @ARGV contains a "-u" entry, use that as the effective userId.  In this 
  729. # case, the userId is the client-side userId that has been passed to this 
  730. # script by the commit_prep script.  (This is why the commit_prep script must 
  731. # be placed *before* the cvs_acls script in the commitinfo admin file.)
  732.  
  733. # Otherwise, pull the userId from the server-side environment.
  734.  
  735.     my $userId = "";
  736.     my ($argv) = shift;             # pick up ref to @ARGV
  737.     my @argvClone = ();             # immutable copy for foreach loop
  738.     for ($i=0; $i<(scalar @{$argv}); $i++) {
  739.     $argvClone[$i]=$argv->[$i]; 
  740.     }
  741.  
  742.     print("$$ \@_ to processArgs is: @_.\n") if $debug;
  743.  
  744.     # Parse command line arguments (file list is seen as one arg)
  745.     foreach $arg (@argvClone) {
  746.     print("$$ \$arg for processArgs loop is: $arg.\n") if $debug;
  747.     # Set $debug flag?
  748.     if ($arg eq '-d') {
  749.         shift @ARGV;
  750.         $debug = 1;
  751.         print("$$ \$debug flag set on.\n") if $debug;
  752.         print STDERR "Debug turned on...\n";
  753.     }
  754.     # Passing in a client-side userId?
  755.     elsif ($arg eq '-u') {
  756.         shift @ARGV;
  757.         $userId = shift @ARGV;
  758.         print("$$ client-side \$userId set to: $userId.\n") if $debug;
  759.     } 
  760.     # An override for the default restrictlog file?
  761.     elsif ($arg eq '-f') {
  762.         shift @ARGV;
  763.         $restrictlog = shift @ARGV;
  764.     } 
  765.     else {
  766.         next;
  767.     }
  768.     }
  769.  
  770.     # No client-side userId passed? then get from server env
  771.     if (!$userId) {
  772.         $userId = $ENV{"USER"} if !($userId = $ENV{"LOGNAME"});
  773.         print("$$ server-side \$userId set to: $userId.\n") if $debug;
  774.     }
  775.  
  776.     print("$$ processArgs returning \$userId: $userId.\n") if $debug;
  777.     return $userId;
  778.  
  779. }
  780.  
  781.  
  782. # ----------------------------------------------------------------------------
  783. # --------------------- Check all modules in list for either file or directory
  784. # ----------------------------------------------------------------------------
  785. sub checkFileness {
  786.  
  787. # Module patterns on the 'cvsacl' record can be files or directories. 
  788. # If it's a directory, we pattern-match the directory name from 'cvsacl' 
  789. # against the left side of the committed filename to see if the file is in 
  790. # that hierarchy.  By contrast, files use an explicit match.  If the entries
  791. # are neither files nor directories, then the cvsacl file has been set up
  792. # incorrectly; we return a "" and the caller skips that line as invalid.
  793. #
  794. # This function determines whether the entries on the 'cvsacl' record are all
  795. # directories or all files; it cannot be a mixture.  This restriction put in
  796. # to simplify the logic (without taking away much functionality).
  797.  
  798.     my @module_list = @_;
  799.     print("$$ Checking \"fileness\" or \"dir-ness\" for \@module_list entries.\n") if $debug;
  800.     print("$$     Entries are: ", join("\, ",@module_list), ".\n") if $debug;
  801.     my $filetype = "";
  802.     for $cvsacl_module (@module_list) {
  803.         my $reposDirName = $cvsroot . '/' . $cvsacl_module;
  804.         my $reposFileName = $reposDirName . "\,v";
  805.         print("$$ In checkFileness: \$reposDirName: $reposDirName; \$reposFileName: $reposFileName.\n") if $debug;
  806.         if (((-d $reposDirName) && ($filetype eq "file")) || ((-f $reposFileName) && ($filetype eq "dir"))) {
  807.             print("Can\'t mix files and directories on single \'cvsacl\' file record; skipping entry.\n");
  808.         print("    Please contact a CVS administrator.\n");
  809.         $filetype = "";
  810.         last;
  811.         }
  812.         elsif (-d $reposDirName) { 
  813.             $filetype = "dir";
  814.         print("$$ $reposDirName is a directory.\n") if $debug;
  815.     }
  816.         elsif (-f $reposFileName) {
  817.             $filetype = "file";
  818.         print("$$ $reposFileName is a regular file.\n") if $debug;
  819.     }
  820.         else {
  821.             print("***** Item to commit was neither a regular file nor a directory.\n");
  822.         print("***** Current \'cvsacl\' line ignored.\n");
  823.         print("***** Possible problem with \'cvsacl\' admin file. Please contact a CVS administrator.\n");
  824.         $filetype = "";
  825.         $text = sprintf("Module entry on cvsacl line: %s is not a valid file or directory.\n", $cvsacl_module);
  826.         write_restrictlog_record($text);
  827.         last;
  828.         } # end if
  829.     } # end for
  830.  
  831.     print("$$ checkFileness will return \$filetype: $filetype.\n") if $debug;
  832.     return $filetype;
  833. }
  834.  
  835.  
  836. # ----------------------------------------------------------------------------
  837. # ----------------------------------------------------- check for module match
  838. # ----------------------------------------------------------------------------
  839. sub checkModuleMatch {
  840.  
  841. # This subroutine checks for a match between the directory or file pattern 
  842. # specified in the 'cvsacl' file (i.e., $cvsacl_modules) versus the commit file 
  843. # objects passed into the script via @ARGV (i.e., $commit_object). 
  844.  
  845. # The directory pattern only has to match the beginning portion of the commit 
  846. # file's name for a match since all files under that directory are considered 
  847. # a match. File patterns must exactly match.
  848.  
  849. # Since (theoretically, if not normally in practice) a working directory can
  850. # contain a mixture of files from different branches, this routine checks to 
  851. # see if there is also a match on branch before considering the file 
  852. # comparison a match.
  853.  
  854.     my $match_flag = "";
  855.  
  856.     print("$$ \@_ in checkModuleMatch is: @_.\n") if $debug;
  857.     my ($type,$commit_object,$cvsacl_module) = @_;
  858.  
  859.     if ($type eq "file") {             # Do exact file match of $commit_object
  860.     if ($commit_object eq $cvsacl_module) {
  861.         $match_flag = "file";
  862.     }                        # Do dir match at beginning of $commit_object
  863.     }
  864.     elsif ($commit_object =~ /^$cvsacl_module\//) {
  865.         $match_flag = "dir";
  866.     }
  867.  
  868.     if ($match_flag) {
  869.     print("$$ \$repository: $repository matches \$commit_object: $commit_object.\n") if $debug;
  870.     if (!$cvsacl_branches) {             # empty branch pattern matches all
  871.         print("$$ blank \'cvsacl\' branch matches all commit files.\n") if $debug;
  872.         $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module];
  873.         print("$$ \$repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module].\n") if $debug;
  874.     }
  875.     else {                             # otherwise check branch hash table
  876.         @branch_list = split (/[\s,]+/,$cvsacl_branches);
  877.         print("$$ Branches from \'cvsacl\' record: ", join(", ",@branch_list),".\n") if $debug;
  878.         if (grep(/$branch{$commit_object}/, @branch_list)) {
  879.         $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module];
  880.         print("$$ \$repository_matches{$commit_object} = [$branch{$commit_object}, " .
  881.                       "$cvsacl_module].\n") if $debug;
  882.         }
  883.     }
  884.     }
  885.  
  886. }
  887.  
  888. # ----------------------------------------------------------------------------
  889. # ------------------------------------------------------- check for file match
  890. # ----------------------------------------------------------------------------
  891. sub printOptionalRestrictionMessage {
  892.  
  893. # This subroutine optionally prints site-specific file restriction information
  894. # whenever a restriction condition is met.  If the file 'restrict_msg' does 
  895. # not exist, the routine immediately exits.  If there is a 'restrict_msg' file
  896. # then all the contents are printed at the end of the standard restriction 
  897. # message.
  898.  
  899. # As seen from examining the definition of $restrictfile, the default filename
  900. # is: $CVSROOT/CVSROOT/restrict_msg.
  901.  
  902.     open (RESTRICT, $restrictfile) || return;    # It is ok for cvsacl file not to exist
  903.     while (<RESTRICT>) {
  904.     chop;
  905.     # print out each line
  906.     print("**** $_\n");
  907.     }
  908.  
  909. }
  910.  
  911. # ----------------------------------------------------------------------------
  912. # ---------------------------------------------------------- write log message
  913. # ----------------------------------------------------------------------------
  914. sub write_restrictlog {
  915.  
  916. # This subroutine iterates through the list of restricted entries and logs 
  917. # each one to the error logfile.
  918.  
  919.     # write each line in @text out separately
  920.     foreach $commitfile (keys %restricted_entries) {
  921.     $log_text = sprintf "Commit attempt by: %s for: %s on branch: %s", 
  922.                         $user_name, $commitfile, $branch{$commitfile};
  923.     write_restrictlog_record($log_text);
  924.     }
  925.  
  926. }
  927.  
  928. # ----------------------------------------------------------------------------
  929. # ---------------------------------------------------------- write log message
  930. # ----------------------------------------------------------------------------
  931. sub write_restrictlog_record {
  932.  
  933. # This subroutine receives a scalar string and writes it out to the 
  934. # $restrictlog file as a separate line. Each line is prepended with the date 
  935. # and time in the format: "2004/01/30 12:00:00 ".
  936.  
  937.     $text = shift;
  938.  
  939.     # return quietly if there is a problem opening the log file.
  940.     open(FILE, ">>$restrictlog") || return;
  941.  
  942.     (@time) = localtime();
  943.  
  944.     # write each line in @text out separately
  945.     $log_record = sprintf "%04d/%02d/%02d %02d:%02d:%02d %s.\n", 
  946.                       $time[5]+1900, $time[4]+1, $time[3], $time[2], $time[1], $time[0], $text;
  947.     print FILE $log_record;
  948.     print("$$ restrict_log record being written: $log_record to $restrictlog.\n") if $debug;
  949.     
  950.     close(FILE);
  951. }
  952.